package com.spacing.app;

import java.util.ArrayList;
import java.util.Date;

import android.database.Cursor;

import com.spacing.app.Tables.SpacerData;
import com.spacing.app.data.Word;

/*
 * @author Tom
 * @author Paul	
 */

public class Spacer {
	// Spacing model variables
	private static double C = 0.25; // decay scale array to start with
	private static int LOOK_AHEAD_TIME = 15;
	private static double THRESHOLD = -0.8;
	private static double STANDARD_ALPHA = 0.32;
	private static int END_OF_SESSION = 180;
	boolean stop = true;
	private boolean firstSession = true;
	private static double STANDARD_RT = 1;
	private static double F = 1; // ?
	

	// Lists
	private SchemaHelper sh;
	private ArrayList<Word> words;
	private boolean trainingComplete;
	private static long psychologicalTime = 0; // time between sessions
	private int nActivations;
	private int lastPairSeen; // index of last seen word
	private boolean lastAnswerCorrect;
	long startOfEventTime; // setter voor maken
	private long startOfSessionTime;
	private long lastWordTime;
	
	private long previousSessionTime;

	private int[] kActivations;
	// number of activations per index (per pair) Functie voor maken??
	// eens
	private String method;
	
	private ArrayList<ArrayList<Double>> arrAlpha;
	//private double[][] arrAlpha;
	private long[][] arrActivationsTime;
	private ArrayList<ArrayList<Long>> arrActivationsTime_t;
	
	private double[][] arrActivationValue; // wordt niet gebruikt???
	private double[][] arrDecayValuesRecentAlpha;
	private long [][]arrFirstKeyPressWithOlderSessions;
	private long [][]arrActivationTimeX;
	private int nWordsInStack;
	
	// Functie voor maken?
	// eens

	public Spacer(ArrayList<Word> words, SchemaHelper sh, String method) {
		this.sh = sh;
		this.words = words;
		this.method = method;
		nWordsInStack = 0;
		// initialize the variables
		initVariables();
	}

	/*
	 * Gaat alle woorden af en vult de arrays kActivations,
	 * kActivationsOlderSessions en vult de decay rates TODO: arrays vervangen
	 * door arraylists oid?
	 */
	private void initVariables() {
		kActivations = new int[words.size()];
		startOfSessionTime = System.currentTimeMillis();
		//arrAlpha = new double[words.size()][];
		arrAlpha = new ArrayList<ArrayList<Double>>();
		arrActivationsTime_t = new ArrayList<ArrayList<Long>>();
		// times on which the word was seen
		arrActivationsTime = new long[words.size()][];
		arrDecayValuesRecentAlpha = new double[words.size()][];
		arrActivationValue = new double[words.size()][];
		int size = 100; // willekeurig gekozen getal. Is nodig om een array
						// lengte aan te geven. Vervangen door Arraylist?
		for (int i = 0; i < words.size(); i++) {
			arrAlpha.add(new ArrayList<Double>());
			arrActivationsTime_t.add(new ArrayList<Long>());
			
			//arrAlpha_t.get(i).add(0.25);
			
			//arrAlpha[i] = new double[size];
			arrActivationsTime[i] = new long[size * 10];
			arrDecayValuesRecentAlpha[i] = new double[size * 10];
			arrActivationValue[i] = new double[size * 10];
			//arrAlpha[i][0] = 0.25;
			kActivations[i] = 0;
		}
		getOldSessionData();
	}

	/*
	 * method that is called to when an answer is given. add new activatonTime
	 * add new alpha add new decay values
	 */
	public void updateAnswer(boolean isCorrect, long time) {
		long activationTime;

		ArrayList<Double> arrAlphaLastPairSeen = arrAlpha.get(lastPairSeen);
		
		
		
		/*
		 * penalty voor fout antwoord. Moet op betere manier ingebouwd worden
		 * (alpha?)
		 */
		if (!isCorrect && kActivations[lastPairSeen] != 0) {
			kActivations[lastPairSeen]--;
		}
		nActivations = kActivations[lastPairSeen];
		lastAnswerCorrect = isCorrect;
		if (isCorrect) {
			activationTime = time - startOfSessionTime + psychologicalTime;
		} else {
			activationTime = startOfEventTime - startOfSessionTime + psychologicalTime;
		}

		//arrActivationsTime[lastPairSeen][nActivations] = activationTime;
		arrActivationsTime_t.get(lastPairSeen).add(activationTime);
		
		//arrAlpha[lastPairSeen][nActivations] = getAlpha(lastPairSeen, (activationTime / 1000), STANDARD_ALPHA);
		
		arrAlphaLastPairSeen.add(getAlpha(lastPairSeen, (activationTime / 1000), STANDARD_ALPHA));
		
		
		//calculateNewDecayValues(lastPairSeen, arrAlpha[lastPairSeen][nActivations]);
		calculateNewDecayValues(lastPairSeen, arrAlphaLastPairSeen.get(arrAlphaLastPairSeen.size()-1));
		kActivations[lastPairSeen]++;
		writeData();
	}// updateAnswer

	private void writeData() {
		new Thread(new Runnable() {
			public void run() {
				/*sh.addSpacerData(words.get(lastPairSeen).getWord1(), 
						method, 
						lastAnswerCorrect, 
						System.currentTimeMillis(),
						arrAlpha[lastPairSeen][nActivations], 
						arrActivationsTime[lastPairSeen][nActivations],
						arrActivationValue[lastPairSeen][nActivations],
						arrDecayValuesRecentAlpha[lastPairSeen][nActivations]);
				*/
				sh.addSpacerData(words.get(lastPairSeen).getWord1(), 
						method, 
						lastAnswerCorrect, 
						System.currentTimeMillis(),
						arrAlpha.get(lastPairSeen).get(nActivations), 
						arrActivationsTime_t.get(lastPairSeen).get(nActivations),
						arrActivationValue[lastPairSeen][nActivations],
						arrDecayValuesRecentAlpha[lastPairSeen][nActivations]);
				
			}
		}).start();
	}

	// calculates optimal alpha. Doet nog geen fuck Yehees
	private double getAlpha(int index, long activation_time, double alpha) {
		double result = alpha;
		return result;
	}

	/*
	 * Real model functions. Function to recalculate new decay values after a
	 * pair has been seen
	 */
	private void calculateNewDecayValues(int index, double alpha) {
		ArrayList<Long> arrActivationsTimeIndex = arrActivationsTime_t.get(index); 
		double[] activations;
		if (kActivations[index] > 0) {
			activations = new double[kActivations[index] + 1];
		} else {
			activations = new double[1];
		}

		double[] encounters = new double[kActivations[index]];

		for (int i = 0; i <= kActivations[index]; i++) {
			if (i > 0) {
				//activations[i] = getActivation(encounters, (arrActivationsTime[index][i] / 1000),arrDecayValuesRecentAlpha[index], i);
				activations[i] = getActivation(encounters, (arrActivationsTimeIndex.get(i) / 1000),arrDecayValuesRecentAlpha[index], i);
				
				if (i < encounters.length) {
					//encounters[i] = (arrActivationsTime[index][i] / 1000);
					encounters[i] = (arrActivationsTimeIndex.get(i) / 1000);
				}
				arrDecayValuesRecentAlpha[index][i] = getDecay(activations[i], alpha);
			} else {
				activations[i] = -1000;
				arrDecayValuesRecentAlpha[index][i] = getDecay(activations[i], alpha);
				if (encounters.length != 0) {
					//encounters[i] = (arrActivationsTime[index][i] / 1000);
					encounters[i] = (arrActivationsTimeIndex.get(i) / 1000);
				}
			}
		}
	} // calculateNewDecayValues

	/*
	 * SpacingActivation returns recent activation value of a pair uses
	 * activation times and decay rates
	 * 
	 * @param: wordnumber index, current time
	 */
	double getSpacingActivation(int index, long time, double alpha) {
		double activationValue = 0;
		double sum = 0;
		double[] encounters = new double[kActivations[index]];
		for (int i = 0; i < kActivations[index]; i++) {
			//encounters[i] = (arrActivationsTime[index][i] / 1000);
			encounters[i] = (arrActivationsTime_t.get(index).get(i) / 1000);
			
			sum = sum + Math.pow((time - encounters[i]), -arrDecayValuesRecentAlpha[index][i]);
		}
		activationValue = Math.log(sum);
		return activationValue;

	}// getSpacingActivation

	/*
	 * calculate activation value based on the timestamps of encounters, current
	 * time, decay values returns an activation value an item for the current
	 * time (time), high activation means that recall will be accurate and fast.
	 * Activation is based on the summation of the individual practive events. -
	 * encounter contains an array of timestamps on which the item was
	 * encountered - time is the current time - decays contains an array of
	 * decay values for each encounter - s is the index: if s is 2 activation is
	 * calculated for the second encounter
	 */
	private double getActivation(double[] encounters, long time, double[] decays, int s) {
		double sum = 0;
		double activation = 0;
		for (int i = 0; i < s; i++) {
			sum = (sum + Math.pow(((time - encounters[i])), -decays[i]));
		}
		activation = Math.log(sum);
		return activation;
	}// getActivation

	// calculate decay value based on the activation and the alfa value
	// (constant and also the minimum decay value)
	private double getDecay(double activation, double alpha) {
		double decay = C * Math.exp(activation) + alpha;
		return decay;
	}// get decay

	/*
	 * determines the next pair looks for the the pair with the lowest
	 * activation (excluding new pairs) based on the lookahead time the methods
	 * checks if the activation will be below the threshold. if so then this is
	 * the next pair. if none of the previously seen pairs comes below the
	 * threshold and there are new pairs then select a new pair. If everything
	 * is seen at least once, then select the pair with the oldest repetition
	 */
	public int determineNextPair() {
		int nextPair = 0;
		double lowestActivation = Double.MAX_VALUE;
		double activationValue;
		// double previousAlpha;
		int indexLowestActivation = 0;
		long time = (System.currentTimeMillis() - startOfSessionTime + psychologicalTime + LOOK_AHEAD_TIME * 1000) / 1000;
		double[] arrTempAcValue = new double[words.size()];
		for (int i = 0; i < words.size(); i++) {
			if (kActivations[i] > 0) {
				if((i+1)>nWordsInStack){
					nWordsInStack = (i+1);
				}
				
				// calculate activation
				//double previousAlpha = arrAlpha[i][(kActivations[i] - 1)];
				double previousAlpha = arrAlpha.get(i).get(kActivations[i] - 1);
				
				// if last time already shown and
				if ((lastPairSeen == i) && (lastAnswerCorrect)) {
					activationValue = 100;
				} else {
					activationValue = getSpacingActivation(i, time, previousAlpha);
					arrTempAcValue[i] = activationValue;
				}

				if (activationValue < lowestActivation) {
					lowestActivation = activationValue;
					indexLowestActivation = i;
				}
			}
		}

		if (lowestActivation < THRESHOLD) {
			// try to get lowest activation below threshold
			nextPair = indexLowestActivation;
		} else if (unshownPairs()) {
			// get the next unshown pair. See if there are encounters in
			// database. If so, determine next pair again
			for (int i = 0; i < words.size(); i++) {
				if (kActivations[i] == 0) {
					if (getWordData(i)) {
						return determineNextPair();
					} else {
						lastPairSeen = i;
						return i;
					}
				}
			}
		} else {
			// all pairs seen so get the one that has the oldest repetition
			trainingComplete = done();
			nextPair = oldestRepetition();
		}
		lastPairSeen = nextPair;
		return nextPair;
	}// determineNextPair

	public boolean isComplete(){
		return trainingComplete;
	}
	
	
	/*
	 * calculate activation further ahead to see if training is done
	 */
	private boolean done(){
		int nextPair = 0;
		double lowestActivation = Double.MAX_VALUE;
		double activationValue;
		// double previousAlpha;
		int indexLowestActivation = 0;
		long time = (System.currentTimeMillis() - startOfSessionTime + psychologicalTime + LOOK_AHEAD_TIME * 1000 + END_OF_SESSION * 1000) / 1000;
		double[] arrTempAcValue = new double[words.size()];
		for (int i = 0; i < words.size(); i++) {
			if (kActivations[i] > 0) {		
				//double previousAlpha = arrAlpha[i][(kActivations[i] - 1)];
				double previousAlpha = arrAlpha.get(i).get(kActivations[i] - 1);
				activationValue = getSpacingActivation(i, time, previousAlpha);
				arrTempAcValue[i] = activationValue;
				

				if (activationValue < lowestActivation) {
					lowestActivation = activationValue;
					indexLowestActivation = i;
				}
			}
		}
		if (lowestActivation > THRESHOLD) {
			// try to get lowest activation below threshold
			return true;
		}
		
		return false;
	}
	
	
	/*
	 * returns true if there are still unshown pairs
	 */
	private boolean unshownPairs() {
		for (int i = 0; i < words.size(); i++) {
			if (kActivations[i] == 0) {
				return true;
			}
		}
		return false;
	}

	/*
	 * returns the chunk that has the oldest repetition
	 */
	private int oldestRepetition() {
		int oldestPair = 0;
		long oldestRep = Long.MAX_VALUE;
		long at;
		for (int i = 0; i < words.size(); i++) {
			if (kActivations[i] > 0) {
				//at = arrActivationsTime[i][(kActivations[i] - 1)];
				at = arrActivationsTime_t.get(i).get(kActivations[i] - 1);
				if (at < oldestRep) {
					oldestRep = at;
					oldestPair = i;
				}
			}
		}
		return oldestPair;
	}

	private void getOldSessionData() {
		for (int i = 0; i < words.size(); i++) {
			Cursor c = sh.getEncounters(words.get(i).getWord1(), method);
			while (c.moveToNext()) {
				int colidAbsTime = c.getColumnIndex(SpacerData.ABSTIME);
				int colidTime = c.getColumnIndex(SpacerData.TIME);
				firstSession = false;
				long st = c.getLong(colidAbsTime);
				if (st > previousSessionTime) {
					previousSessionTime = st;
				}
				long t = c.getLong(colidTime);
				if (lastWordTime < t) {
					lastWordTime = t;
				}
			}
		}
		if (!firstSession) {
			psychologicalTime = startOfSessionTime - previousSessionTime;
		}
		startOfSessionTime = startOfSessionTime - lastWordTime;
	}

	private boolean getWordData(int wordIndex) {
		Cursor c = sh.getEncounters(words.get(wordIndex).getWord1(), method);
		ArrayList<Double> arrAlpha_temp = new ArrayList<Double>();
		ArrayList<Long> arrActivationTime_temp = new ArrayList<Long>();
		while (c.moveToNext()) {
			int colidAlpha = c.getColumnIndex(SpacerData.ALPHA);
			int colidTime = c.getColumnIndex(SpacerData.TIME);
			int colidValue = c.getColumnIndex(SpacerData.VALUE);
			int colidDecay = c.getColumnIndex(SpacerData.DECAY);
			arrAlpha_temp.add(c.getDouble(colidAlpha));
			arrActivationTime_temp.add(c.getLong(colidTime));
			
			//arrAlpha[wordIndex][c.getPosition()] = c.getDouble(colidAlpha);
			//arrActivationsTime[wordIndex][c.getPosition()] = c.getLong(colidTime);
			arrActivationValue[wordIndex][c.getPosition()] = c.getDouble(colidValue);
			arrDecayValuesRecentAlpha[wordIndex][c.getPosition()] = c.getDouble(colidDecay);
			kActivations[wordIndex]++;
		}
		arrAlpha.set(wordIndex, arrAlpha_temp);
		arrActivationsTime_t.set(wordIndex, arrActivationTime_temp);
		return kActivations[wordIndex] > 0;
	}
	
	public int getNumberInStack(){
		return nWordsInStack;
	}

	// NIEUWE FUNCTIES //
	
	//converts reaction times to activations 
	private double rtToActivation(double rt) {
		//reaction time too small, set it at the minimum
		if(rt < STANDARD_RT + 100){
			rt = STANDARD_RT + 100;
		}
		return -Math.log((rt-STANDARD_RT)/(F*1000));
	}
	
	//calculates optimal alfa
	// activationTime in seconds
	private double getAlfa(int i, long activationTime){
	    	
		Date currentTime = new Date();
		long presentTime = currentTime.getTime();
		ArrayList<Double> decaysOnRt = new ArrayList<Double>();
		double oldAlpha;
		double mean_alfa;
		double new_alfa = 0; //???
		double error_old;
		double error_new;
		double error_mean;
		if(kActivations[i] < 4 ){ //number here is the rehearsal at which we start adjusting, since the current encounter has not been added to kActivations yet and the first encounter is study.
			return STANDARD_ALPHA;
		}  	
		//oldAlpha = arrAlpha[i][(kActivations[i]-1)];
		oldAlpha = arrAlpha.get(i).get(kActivations[i]-1);
		
		//get decays for alpha calculation
		decaysOnRt = getDecays(i, arrActivationsTime[i], oldAlpha );
		//get the optimal alpha for this last encoutner
		//new_alfa = get_alfa_from_activation(i, arrActivationsOnRtWithOlderSessions[i], decaysOnRt, arrActivationTimeSoundOrderedWithOlderSessions[i], activationTime, old_alfa);
	 	mean_alfa = (oldAlpha + new_alfa)/2; 
		 //calculate the errors for all alternatives
		error_old = getDistance(i, arrFirstKeyPressWithOlderSessions[i], arrActivationTimeX[i], activationTime, oldAlpha);
	 	error_new  = getDistance(i, arrFirstKeyPressWithOlderSessions[i], arrActivationTimeX[i], activationTime, new_alfa);
	 	error_mean  = getDistance(i, arrFirstKeyPressWithOlderSessions[i], arrActivationTimeX[i], activationTime, mean_alfa);
	    
	 	//quick greedy search for the global optimal alpha value between the best alpha for the current encounter and the best alpha for the previous series op encoutners.
		for(int k =0; k<6; k++){ 
			if(error_mean > error_old){ //mean has bigger error, alfa between mean and old
				new_alfa = mean_alfa;
				mean_alfa = (oldAlpha+mean_alfa)/2 ;
				error_new = error_mean;
			}
			if(error_mean < error_old){	//mean does better than old, alfa between mean and new
				oldAlpha = mean_alfa;
				mean_alfa = (new_alfa+mean_alfa)/2;
				error_old = error_mean;
			}      
			error_mean = getDistance(i, arrFirstKeyPressWithOlderSessions[i], arrActivationTimeX[i], activationTime, mean_alfa);
		}	
		//some book keeping
		currentTime = new Date();
		long endTime = currentTime.getTime();
		long dif = endTime - presentTime;    
		return mean_alfa;
	}//get alfa
	
	//i is wordpair, activations are activations on reactiontimes, decays are decasy on alfa and reactiontimes
	//i =  wordpair number
	//activations are activations on RT 
	//decays are decays on acitvations on rt
	//encounters are ALL encounters, including time
	//time is last encounter in seconds
	// alfa is last alfa
	//
	private double getAlfaFromActivation(int i, double [] activations, double [] decays, double [] encounters, long time, double alfa){
		double activation_last_encounter;
		double activation_second_last_encounter;
		double summationEncounters;
		double summmationWithoutLastEncounter;
		double observedSummation;
		double difference;
		double timespan;
		double observedalfa;
		double decay;
		double observedAlfa;
		//get the activation for the last encounter
		activation_last_encounter = activations[kActivations[i]];
		//turn it into the summation
		observedSummation = Math.exp(activation_last_encounter);	
		//get the activation at the second last encounter
		activation_second_last_encounter = activations[(kActivations[i]-1)]; 	
		//get the summation without the last encounter
		summmationWithoutLastEncounter = getSumWithoutLastEncounters(i, encounters, time, decays);	
		//difference in activation is the addition of the last encounter to the total activation
		difference = observedSummation - summmationWithoutLastEncounter;	
		//the time span over which this addition has decayed
		timespan = time - (encounters[(kActivations[i]-1)]/1000);
		//cannot calculate with negative numbers
		if(difference < 0){
			observedAlfa = alfa *2;
		//observed alpha is the calculated d - c*e^activation
		}else{
			decay = -(Math.log(difference)/Math.log(timespan));
			observedAlfa = decay - (Math.exp(activation_second_last_encounter)*C);
		}
		//alpha needs to stay positive
		if(observedAlfa <0 ) {
			return 0;
		}
		else{
			return observedAlfa;
		}
	}//get alfa from activation
	
	//calculates sum of different encounters
	//this is part of activation calculation, but is only used like this to calculate difference from these encounters and observed 
	private double getSumWithoutLastEncounters(int i, double [] encounters, long time, double [] decays){
		double sum = 0;
		for(int j =0; j < (kActivations[i]-1); j++){
			sum  += Math.pow((time - (encounters[j]/1000)),(-decays[j]));	
		}
		return sum;
	}//get sum encounters

	
	
	//based on activation from reaction times and alfa, calculate the decays
	private ArrayList<Double> getDecays(int i, long [] activations, double alfa){
		ArrayList<Double> decays = new ArrayList<Double>();
		
		for(int k =0; k < (kActivations[i]+1); k++){ 
		decays.add(C * Math.exp(activations[k]) + alfa);
		}
		return decays;	
	}//get decays
	
	
	//i = wordpair
	//reactionTimes are firstkeypresses
	//encounters are times
	//time is activation time
	//alfa is last alfa

	private double getDistance(int i, long [] reactiontimes, long [] encounters, long time, double alfa){ //
		double [] decay = new double[encounters.length];
		double difference = 0;
		ArrayList<Double> enc = new ArrayList<Double>();
		double activation;
		double s =	 (kActivations[i]-1);
		for(int k=0; k < (kActivations[i]+1); k++){
			if(k==0){
				activation =-1000;
			}else{
				activation = getTempActivation(enc, encounters[k], decay);
	            if(k!=1){//do not take the first rehearse into account, it is unreliable.
				     difference = difference + get_latency_difference(activation, reactiontimes[k]);
	            }
			}
			decay[k] = getDecay(activation, alfa);
			//enc.push(encounters[k]);
		}
		return difference;
	}//get_distance

	private double get_latency_difference(double activation, long rt){
	 	double expectedReactionTime = mToRt(activation);
		return Math.abs(expectedReactionTime - rt);	
	}//get latency difference
	
	
	//from activation to reaction time
	double mToRt(double activation){
		return ((F*Math.exp(-activation) + (STANDARD_RT/1000)) *1000); //to miliseconds
	}//m_to_rt

	//also in ms
	double  getTempActivation(ArrayList<Double> encounters, long time, double [] decays){
		double  sum = 0;
		for(int k=0; k< encounters.size(); k++){
			sum += Math.pow(((time/1000) - (encounters.get(k)/1000)), (-decays[k])); 
		}
		return Math.log(sum);
			
	}//get_temp_activation

}// SpacingMethods
